﻿/////////////////////////////////////////////////////////////////
//
// Physics Helper XAML
//
// by Andy Beaulieu - http://www.andybeaulieu.com
//
// LICENSE: This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ANY 
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS
// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
/////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FarseerPhysics.Collision.Shapes;
using FarseerPhysics.Controllers;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Xaml;
using sys = Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Shapes;
using xna = Microsoft.Xna.Framework;
using System.Diagnostics;


namespace Spritehand.FarseerHelper
{
    /// <summary>
    /// The main Simulation/Game Canvas
    /// </summary>
    public class PhysicsCanvas : Canvas
    {
        const double _fps = 60;
        const double _msPerFrame = 1000d / _fps;

        /// <summary>
        /// The instance of the Farseer Physics World
        /// </summary>
        public World Simulator { get; set; }

        /// <summary>
        /// The List of PhysicsSprites in the simulation
        /// </summary>
        public Dictionary<string, PhysicsSprite> PhysicsObjects { get; set; }

        public delegate void TimerLoopHandler(object source);
        /// <summary>
        /// Event fired for every game loop tick
        /// </summary>
        public event TimerLoopHandler TimerLoop;

        public delegate void CollisionHandler(PhysicsSprite sprite1, PhysicsSprite sprite2, Contact contact);
        /// <summary>
        /// Event fired for every sprite-to-sprite Collision
        /// </summary>
        public event CollisionHandler Collision;

        /// <summary>
        /// The last frame per second calc in the simulation
        /// </summary>
        public int LastFPS { get; set; }
        private DateTime _lastTick;
        private int _frameCounter;

        private static int _simulationCount = 1;
        private int _simulationNumber;

#if !METRO
        private DispatcherTimer _timerLoop;
#endif

        private FrameworkElement _scaleParent;

        private double _relativeScale = 1.0d;
        public double RelativeScale
        {
            get
            {
                return _relativeScale;
            }
            set
            {
                _relativeScale = value;
            }
        }


        // Under certain circumstances, we may want to allow static objects to move
        bool _allowStaticObjectManipulation = false;
        public bool AllowStaticObjectManipulation
        {
            get
            {
                return _allowStaticObjectManipulation;
            }
            set
            {
                _allowStaticObjectManipulation = value;

                foreach (PhysicsSprite spr in PhysicsObjects.Values)
                {
                    spr.AllowStaticObjectManipulation = value;
                }
            }
        }

        public CompositeTransform _transformRoot;
        /// <summary>
        /// For cases where we are scaling size to the screen, 
        /// this is the render transform to perform the scale.
        /// </summary>
        public CompositeTransform TransformRoot
        {
            get
            {
                return _transformRoot;
            }
            set
            {
                _transformRoot = value;
            }
        }

        private static BoundaryHelperBox2d _boundaryHelper;
        /// <summary>
        /// The boundary helper converts between Screen and World (Farseer/Box2D) Coordinates
        /// </summary>
        public static BoundaryHelperBox2d BoundaryHelper
        {
            get
            {
                return _boundaryHelper;
            }
        }

        private bool _pauseSimulation = false;
        /// <summary>
        /// Set PauseSimulation to true to stop the physics simulation.
        /// </summary>
        public bool PauseSimulation
        {
            get
            {
                return _pauseSimulation;
            }
            set
            {
                _pauseSimulation = value;
            }
        }


        public PhysicsCanvas()
        {
            if (Utilities.IsDesignMode)
                return;

            PauseSimulation = true;

            Simulator = new World(new xna.Vector2(GravityHorizontal, GravityVertical));

            _boundaryHelper = new BoundaryHelperBox2d(this);

            PhysicsObjects = new Dictionary<string, PhysicsSprite>();

            // create our "game loop" timer
            CompositionTarget.Rendering += CompositionTarget_Rendering;

            // just a track of number of sims in current app
            _simulationNumber = _simulationCount;
            _simulationCount++;

            this.Loaded += new RoutedEventHandler(PhysicsCanvas_Loaded);
        }

        void CompositionTarget_Rendering(object sender, object e)
        {
            HandleTimerTick((float)TimeStep);
        }

        void _timerLoop_Tick(object sender, object e)
        {
            HandleTimerTick((float)TimeStep);
        }

        void PhysicsCanvas_Loaded(object sender, RoutedEventArgs e)
        {
            PauseSimulation = false;




            if (PhysicsObjects != null)
            {
                // if we already have Physics Objects array then this is likely the result of 
                // a page navigation return.
                return;
            }



        }

        /// <summary>
        /// Ensure that the simulation runs FULL screen, or to the size of the parent container.
        /// </summary>
        /// <param name="parent"></param>
        public void ScaleToObject(FrameworkElement parent)
        {
            _scaleParent = parent;
            _scaleParent.SizeChanged += new SizeChangedEventHandler(parent_SizeChanged);
        }

        private void parent_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            // scale the canvas so we are full screen
            FrameworkElement parentContainer = (sender as FrameworkElement);
            double canvasHeight = parentContainer.ActualHeight;
            double canvasWidth = parentContainer.ActualWidth;

            double scaleX = canvasWidth / this.Width;
            double scaleY = canvasHeight / this.Height;

            if (scaleX > scaleY)
            {
                scaleX = scaleY;
                RelativeScale = scaleX;
            }
            else
            {
                scaleY = scaleX;
                RelativeScale = scaleY;
            }

            _transformRoot = this.RenderTransform as CompositeTransform;
            if (_transformRoot == null)
            {
                _transformRoot = new CompositeTransform();
                this.RenderTransform = _transformRoot;
            }

            //_transformRoot.CenterX = 0;
            //_transformRoot.CenterY = (canvasHeight);

            _transformRoot.ScaleX = scaleX;
            _transformRoot.ScaleY = scaleY;
            _transformRoot.TranslateX = (parentContainer.ActualWidth - this.Width * scaleX) / 2;
            _transformRoot.TranslateY = (parentContainer.ActualHeight - (this.Height * scaleY));


            this.UpdateLayout();
        }

        public void WakeUpAllSprites()
        {
            // wake up all sprites to adjust
            foreach (PhysicsSprite sprite in PhysicsObjects.Values)
            {
                sprite.BodyObject.Awake = true;
            }
        }


        private bool _autoUpdate = true;
        /// <summary>
        /// If true (default) then CompositionTarget.Rendering auto updates the simulation.
        /// Otherwise you must call UpdateTick() yourself.
        /// </summary>
        public bool AutoUpdate
        {
            get
            {
                return _autoUpdate;
            }
            set
            {
                _autoUpdate = value;
            }
        }


        void HandleTimerTick(float step)
        {
            if (PauseSimulation || AutoUpdate == false)
                return;

            // run the physics sim update
            Simulator.Step(step);

            HandleCollisions();

            UpdateDraw();


            // fire the timer tick event 
            if (TimerLoop != null)
                TimerLoop(this);
        }

        private double _timeStep = 0.02D;
        /// <summary>
        /// The amount of time that passes in the simulation for each time step
        /// </summary>
        public double TimeStep
        {
            get { return _timeStep; }
            set { _timeStep = value; }
        }

        private bool _mousePickEnabled = false;
        /// <summary>
        /// If true, then physics elements can be moved around with gestures.
        /// </summary>
        public bool MousePickEnabled
        {
            get { return _mousePickEnabled; }
            set
            {
                _mousePickEnabled = value;

                if (Utilities.IsDesignMode)
                    return;

                foreach (PhysicsSprite spr in PhysicsObjects.Values)
                {
                    spr.MousePickEnabled = value;
                }
            }
        }

        private int _gravityHorizontal = 0;
        public int GravityHorizontal
        {
            get { return _gravityHorizontal; }
            set { _gravityHorizontal = value; }
        }

        private int _gravityVertical = 10;
        public int GravityVertical
        {
            get { return _gravityVertical; }
            set { _gravityVertical = value; }
        }

        public bool _debugMode = false;
        /// <summary>
        /// If true, then the boundaries for physics elements is displayed as an overlay.
        /// </summary>
        public bool DebugMode
        {
            get { return _debugMode; }
            set
            {
                _debugMode = value;

                if (Utilities.IsDesignMode)
                    return;

                foreach (PhysicsSprite spr in PhysicsObjects.Values)
                {
                    spr.DebugMode = value;
                }
            }
        }

        public void HandleCollisions()
        {
            foreach (CollisionInstance collision in CollisionStore.Collisions)
            {
                Collision(collision.Sprite1, collision.Sprite2, collision.Contact);
            }

            CollisionStore.Collisions.Clear();
        }



        /// <summary>
        /// Update the Simulation for a "frame"
        /// </summary>
        public void UpdateDraw()
        {
            foreach (PhysicsSprite item in PhysicsObjects.Values)
            {
                if (item.DebugMode == true || item.BodyObject.Awake || item._focusMode)
                    item.Update();

                if (item.MousePickEnabled == true)
                {
                    item.UpdatePickSpring();
                }
            }



            // track the Frames Per Second
            _frameCounter++;
            if ((DateTime.Now - _lastTick).TotalMilliseconds > 1000)
            {
                LastFPS = _frameCounter;
                _frameCounter = 0;
                _lastTick = DateTime.Now;

                //System.Diagnostics.Debug.WriteLine("world " + _simulationNumber + " fps=" + LastFPS.ToString());
            }

        }


        void polygon_Collision(PhysicsSprite source, string spriteCollided, Contact contact)
        {
            // for now we just STORE the collision because we 
            // cannot destroy or inactivate things during a FP3 Step operation!
            if (Collision != null && this.PhysicsObjects.Keys.Contains(spriteCollided))
            {

                PhysicsSprite sprite2 = this.PhysicsObjects[spriteCollided];
                CollisionStore.AddCollision(source, sprite2, contact);


                if (PhysicsObjects.Keys.Contains(source.Name) == false)
                    throw new Exception("sprite " + source.Name + " is not in the list of sprites");
                if (PhysicsObjects.Keys.Contains(sprite2.Name) == false)
                    throw new Exception("sprite " + sprite2.Name + " is not in the list of sprites");

            }

        }


        /// <summary>
        /// Adds a single Physics Sprite into the Physics simulation.
        /// </summary>
        /// <param name="cnvContainer">The container to add items from.</param>
        public PhysicsSprite AddPhysicsSprite(PhysicsSprite element, bool enablePhysics)
        {
            if (enablePhysics)
            {
                string thisName = element.GetValue(Canvas.NameProperty).ToString();
                element.SetValue(Canvas.TagProperty, thisName); // save original name in tag

                if ((element.Parent as Panel).Children.IndexOf(element) < 0)
                    throw new Exception("Could not find element " + thisName + " in the parent");
                (element.Parent as Panel).Children.Remove(element);

                thisName = PhysicsUtilities.EnsureUniqueName(this, element, thisName);  // get a unique name in the game canvas
                element.Name = thisName;

                //PhysicsUtilities.DebugVisualTree(this, 1);

                this.Children.Add(element);

                PhysicsObjects.Add(thisName, element);
                element.Update();

                element.Collision += new PhysicsSprite.CollisionHandler(polygon_Collision);
            }

            if (MousePickEnabled)
            {
                element.MousePickEnabled = true;
            }

            return element;

        }

        /// <summary>
        /// Adds a Physics Joint to the simulation
        /// </summary>
        /// <param name="joint"></param>
        public void AddPhysicsJoint(PhysicsJoint joint)
        {
            string bodyOne = joint.BodyOne;
            string bodyTwo = joint.BodyTwo;
            short collisionGroup = Convert.ToInt16(joint.CollisionGroup);
            bool isAngleSpringEnabled = joint.AngleSpringEnabled;
            float springConstant = (float)joint.AngleSpringConstant;
            float angleLowerLimit = (float)joint.AngleLowerLimit;
            float angleUpperLimit = (float)joint.AngleUpperLimit;


            Point center = joint.GetCenter();
            xna.Vector2 ptCollisionCenter = new xna.Vector2((float)center.X, (float)center.Y);


            if (!PhysicsObjects.ContainsKey(bodyOne))
                throw new Exception("Cannot add joint for an invalid BodyOne value of '" + bodyOne + "'. If using Behaviors, did you forgot to add a PhysicsObjectBehavior?");
            if (!PhysicsObjects.ContainsKey(bodyTwo))
                throw new Exception("Cannot add joint for an invalid BodyTwo value of '" + bodyTwo + "'. If using Behaviors, did you forgot to add a PhysicsObjectBehavior?");

            Body body1 = PhysicsObjects[bodyOne].BodyObject;
            Body body2 = PhysicsObjects[bodyTwo].BodyObject;

            xna.Vector2 ptCollisionCenterA = _boundaryHelper.ScreenToWorld(ptCollisionCenter);
            xna.Vector2 ptCollisionCenterB = _boundaryHelper.ScreenToWorld(ptCollisionCenter);

            // account for offset within body
            ptCollisionCenterA -= body1.Position;
            ptCollisionCenterB -= body2.Position;

            // DEMO: (5) Create Joints
            if (joint.IsWeldJoint)
            {
                WeldJoint weldJoint = new WeldJoint(body1, body2, ptCollisionCenterA, ptCollisionCenterB);
                Simulator.AddJoint(weldJoint);
            }
            else
                if (joint.IsDistanceJoint)
            {
                DistanceJoint distanceJoint = new DistanceJoint(body1, body2, ptCollisionCenterA, ptCollisionCenterB);

                Simulator.AddJoint(distanceJoint);

            }
            else
            {
                RevoluteJoint revoluteJoint = new RevoluteJoint(body1, body2, ptCollisionCenterA, ptCollisionCenterB);

                Simulator.AddJoint(revoluteJoint);

                if (isAngleSpringEnabled)
                {
                    AngleJoint aj = new AngleJoint(body1, body2);
                    aj.TargetAngle = 0;
                    aj.Softness = springConstant;


                    Simulator.AddJoint(aj);

                }

                if (angleUpperLimit != -1 && angleLowerLimit != -1)
                {
                    float upperAngle = (float)PhysicsUtilities.DegreesToRadians(angleUpperLimit);
                    float lowerAngle = (float)PhysicsUtilities.DegreesToRadians(angleLowerLimit);

                    revoluteJoint.LimitEnabled = true;
                    revoluteJoint.LowerLimit = lowerAngle;
                    revoluteJoint.UpperLimit = upperAngle;
                }
            }

            if (collisionGroup > 0)
            {

                foreach (Fixture f in PhysicsObjects[bodyOne].BodyObject.FixtureList)
                {
                    f.CollisionGroup = collisionGroup;
                }
                foreach (Fixture f in PhysicsObjects[bodyTwo].BodyObject.FixtureList)
                {
                    f.CollisionGroup = collisionGroup;
                }
            }

            // get rid of the UI representation of the joint
            joint.Visibility = Visibility.Collapsed;

        }

        /// <summary>
        /// Add all defined PhysicsSprites contained in a Usercontrol.
        /// IMPORTANT: You should track a list of all UserControls added so that you can clean them up
        /// at the end of a "Level" - to clean up, just do a PhysicsCanvas.Children.Remove(uc);
        /// </summary>
        /// <param name="uc">The UserControl to add, containing 1 or more PhysicsSprite and PhysicsJoint objects</param>
        /// <param name="left">The screen X position to add at.</param>
        /// <param name="top">The screen Y position to add at.</param>
        public void AddPhysicsUserControl(UserControl uc, double left, double top)
        {
            AddPhysicsUserControl(uc, left, top, -1);
        }

        /// <summary>
        /// Add all defined PhysicsSprites contained in a Usercontrol.
        /// IMPORTANT: You should track a list of all UserControls added so that you can clean them up
        /// at the end of a "Level" - to clean up, just do a PhysicsCanvas.Children.Remove(uc);
        /// </summary>
        /// <param name="uc">The UserControl to add, containing 1 or more PhysicsSprite and PhysicsJoint objects</param>
        /// <param name="left">The screen X position to add at.</param>
        /// <param name="top">The screen Y position to add at.</param>
        /// <param name="collisionGroup">The collisiongroup - increment this to make all like sprites collide.</param>
        public List<PhysicsSprite> AddPhysicsUserControl(UserControl uc, double left, double top, int collisionGroup)
        {
            List<PhysicsSprite> spriteList = new List<PhysicsSprite>();
            Canvas.SetLeft(uc, left);
            Canvas.SetTop(uc, top);

            PhysicsUtilities.RedirectStoryboardTargets(uc);
            PhysicsUtilities.EnsureUniqueNames(uc, Guid.NewGuid(), collisionGroup, spriteList);  // get a unique name in the game canvas

            this.Children.Add(uc);

            foreach (PhysicsSprite spr in spriteList)
            {
                spr.UserControlParent = uc;
            }

            return spriteList;
        }

        /// <summary>
        /// Convert from a Screen point to a Farseer/Box2D world point
        /// </summary>
        public xna.Vector2 ScreenToWorld(Point screen)
        {
            return _boundaryHelper.ScreenToWorld(new xna.Vector2((float)screen.X, (float)screen.Y));
        }

        /// <summary>
        /// Convert from a Farseer/Box2D world point to a Screen point
        /// </summary>
        public xna.Vector2 WorldToScreen(xna.Vector2 world)
        {
            return _boundaryHelper.WorldToScreen(world);
        }

        /// <summary>
        /// Completely delete a sprite
        /// </summary>
        /// <param name="sprite"></param>
        public void DeletePhysicsObject(string sprite)
        {
            bool found = PhysicsObjects.Where(e => e.Key == sprite).Count() > 0;
            if (found)
            {
                PhysicsSprite spr = PhysicsObjects[sprite];
                spr.IsEnabled = false;
                spr.Cleanup();

                spr.Collision -= (polygon_Collision);

                if (Simulator.BodyList.Contains(spr.BodyObject))
                    DeleteObject(spr.BodyObject);

                // remove the parent user control if it existed
                if (spr.UserControlParent != null)
                {
                    this.Children.Remove(spr.UserControlParent);
                }

                PhysicsObjects.Remove(sprite);
            }

            UIElement elem = this.FindName(sprite) as UIElement;
            if (elem != null)
            {
                this.Children.Remove(this.FindName(sprite) as UIElement);
            }

        }

        /// <summary>
        /// Delete all sprites, clean up, generally tidy everything.
        /// </summary>
        public void CleanUp()
        {
            for (int i = PhysicsObjects.Values.Count - 1; i >= 0; i--)
            {
                PhysicsSprite spr = PhysicsObjects.Values.Last();
                DeletePhysicsObject(spr.Name);
            }

#if !METRO && !SILVERLIGHT
            _timerLoop.Stop();
            _timerLoop.Tick -= _timerLoop_Tick;
#endif
            if (_scaleParent != null)
            {
                _scaleParent.SizeChanged -= parent_SizeChanged;
            }
            this.Loaded -= PhysicsCanvas_Loaded;

        }

        /// <summary>
        /// Simple wrapper for deleting various physics objects
        /// </summary>
        private void DeleteObject(object o)
        {
            if (o is Body)
                Simulator.RemoveBody(o as Body);
            else
                if (o is Controller)
                Simulator.RemoveController(o as Controller);
            else
                    if (o is Joint)
                Simulator.RemoveJoint(o as Joint);
            else
                throw new Exception("DeleteObject can only delete Body, Controller or Joint elements.");
        }

    }
}
